/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.misc.dts.oo.comp;

import com.sun.star.lang.XComponent;
import com.sun.star.lang.XServiceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vacoor.nothing.common.util.Throwables;
import org.vacoor.nothing.misc.dts.oo.util.UnoUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * 用于根据扩展名和 Service 获取对应的 Export Filter
 * <p/>
 * 该类根据 OpenOffice xcd (xml component definition) file 获取支持导出的 Filter
 * 加载默认配置后会加载 META-INF/ooExpFilter.xcd
 * xcd 参见 "$OO_HOME/share/registry" (LibreOffice, OpenOffice 应该类似)
 *
 * @author vacoor
 * @see <decode href="http://cgit.freedesktop.org/libreoffice/core/tree/filter/source/config/fragments/filters">OOo Filters</decode>
 */
public abstract class UnoExpFilterRegistry {
    private static final Logger LOG = LoggerFactory.getLogger(UnoExpFilterRegistry.class);
    private static final String DEFAULT_XCD_PATH = "META-INF/org/vacoor/nothing/misc/dts/oo/defaults";
    private static final String[] DEFAULT_XCD_FILES = {
            DEFAULT_XCD_PATH + "/writer.xcd",
            DEFAULT_XCD_PATH + "/calc.xcd",
            DEFAULT_XCD_PATH + "/impress.xcd",
            DEFAULT_XCD_PATH + "/base.xcd",
            DEFAULT_XCD_PATH + "/draw.xcd",
            DEFAULT_XCD_PATH + "/math.xcd",
            DEFAULT_XCD_PATH + "/graphicfilter.xcd",
            DEFAULT_XCD_PATH + "/xsltfilter.xcd"
    };

    private static Map<Pair<String/*service*/, String/*extension*/>, String> documentExpFilterMap;

    public static String getFilterName(XComponent doc, String extension) {
        XServiceInfo xServiceInfo = UnoUtils.$(XServiceInfo.class, doc);
        if (xServiceInfo == null) {
            return null;
        }
        /*
        if (xServiceInfo.supportsService("com.sun.star.text.GenericTextDocument")) {
            // writer: com.sun.star.text.GenericTextDocument, com.sun.star.text.TextDocument, com.sun.star.document.OfficeDocument
            LOG.info("TEXT");
        } else if (xServiceInfo.supportsService("com.sun.star.sheet.SpreadsheetDocument")) {
            // calc: com.sun.star.sheet.SpreadsheetDocument, com.sun.star.sheet.SpreadsheetDocumentSettings
            LOG.info("SPREADSHEET");
        } else if (xServiceInfo.supportsService("com.sun.star.formula.FormulaProperties")) {
            // math: com.sun.star.formula.FormulaProperties, com.sun.star.document.OfficeDocument
            LOG.info("FORMULA");
        } else if (xServiceInfo.supportsService("com.sun.star.presentation.PresentationDocument")) {
            LOG.info("PRESENTATION");
        } else if (xServiceInfo.supportsService("com.sun.star.drawing.DrawingDocument")) {
            // draw: com.sun.star.drawing.GenericDrawingDocument, com.sun.star.drawing.DrawingDocumentFactory, com.sun.star.drawing.DrawingDocument, com.sun.star.document.OfficeDocument
            LOG.info("DRAWING");
        } else if (xServiceInfo.supportsService("com.sun.star.sdb.OfficeDatabaseDocument")) {
            // base: com.sun.star.sdb.OfficeDatabaseDocument, com.sun.star.document.OfficeDocument
            LOG.info("DATABASE");
        } else {
            LOG.warn("UNKNOW");
        }
        */
        String[] serviceNames = xServiceInfo.getSupportedServiceNames();
        for (String service : serviceNames) {
            String filterName = getFilterName(service, extension);
            if (filterName != null) {
                return filterName;
            }
        }
        return null;
    }

    public static String getFilterName(String service, String extension) {
        if (service == null || extension == null) {
            return null;
        }
        ensureExpFilterMap();
        return documentExpFilterMap.get(Pair.of(service, extension));
    }

    private static void ensureExpFilterMap() {
        synchronized (UnoExpFilterRegistry.class) {
            if (documentExpFilterMap != null) {
                return;
            }
            documentExpFilterMap = new HashMap<Pair<String/*service*/, String/*extension*/>, String>();
            // 加载默认配置
            for (String defaultXcdFile : DEFAULT_XCD_FILES) {
                loadResource(UnoExpFilterRegistry.class.getClassLoader().getResourceAsStream(defaultXcdFile), documentExpFilterMap);
            }

            // 加载用户配置
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            try {
                Enumeration<URL> resources = cl.getResources("META-INF/ooExpFilter.xcd");
                while (resources.hasMoreElements()) {
                    URL url = resources.nextElement();
                    loadResource(url.openStream(), documentExpFilterMap);
                }
            } catch (IOException ignore) {
                // ignore
            }
        }
    }

    static void loadResource(InputStream is, Map<Pair<String/*service*/, String/*extension*/>, String> filterMap) {
        LOG.info("Load xcd resource: {}", is);
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(false); // never forget this!
            DocumentBuilder builder = factory.newDocumentBuilder();

            Document doc = builder.parse(is);
            XPathFactory xPathFactory = XPathFactory.newInstance();
            XPath xPath = xPathFactory.newXPath();

            XPathExpression filtersXpath = xPath.compile("/data/component-data[@package='org.openoffice.TypeDetection'][@name='Filter']/node[@name='Filters']/node[prop[@name='Flags']/value[contains(text(), 'EXPORT')]]");
            XPathExpression nameAttrXpath = xPath.compile("attribute::name");
            XPathExpression typePropXpath = xPath.compile("prop[@name='Type']/value/text()");
            XPathExpression docSvPropXpath = xPath.compile("prop[@name='DocumentService']/value/text()");
            XPathExpression preferredPropXpath = xPath.compile("prop[@name='Preferred']/value/text()");
            XPathExpression extensionsPropXpath = xPath.compile("prop[@name='Extensions']/value/text()");
            XPathExpression extensionsSepXPath = xPath.compile("prop[@name='Extensions']/value/attribute::separator");

            NodeList filterNodes = (NodeList) filtersXpath.evaluate(doc, XPathConstants.NODESET);

            for (int i = 0; i < filterNodes.getLength(); i++) {
                org.w3c.dom.Node item = filterNodes.item(i);
                String name = (String) nameAttrXpath.evaluate(item, XPathConstants.STRING);
                String type = (String) typePropXpath.evaluate(item, XPathConstants.STRING);
                String service = (String) docSvPropXpath.evaluate(item, XPathConstants.STRING);
                Element typeNode = (Element) xPath.compile("/data/component-data[@package='org.openoffice.TypeDetection'][@name='Types']/node[@name='Types']/node[@name='" + type + "']").evaluate(doc, XPathConstants.NODE);
                String preferred = preferredPropXpath.evaluate(typeNode);
                String extensions = extensionsPropXpath.evaluate(typeNode);
                String separator = extensionsSepXPath.evaluate(typeNode);
                separator = separator != null && separator.length() > 0 ? separator : " ";

                LOG.debug("type: {}, extensions: {}(sep: {}), service: {}, filter: {}", type, extensions, separator, service, name);
                for (String extension : extensions.split(Pattern.quote(separator))) {
                    Pair key = Pair.of(service, extension);
                    if (filterMap.get(key) != null && !"true".equals(preferred)) {
                        continue;
                    }
                    filterMap.put(Pair.of(service, extension), name);
                }
            }
        } catch (Exception e) {
            throw Throwables.unchecked(e);
        } finally {
            LOG.info("Load xcd over.");
        }
    }

    // -----------------
    private static class Pair<F, S> {
        private final F first;
        private final S second;

        public static <F, S> Pair of(F first, S second) {
            return new Pair(first, second);
        }

        Pair(F first, S type) {
            this.first = first;
            this.second = type;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Pair)) return false;

            Pair pair = (Pair) o;

            if (first != null ? !first.equals(pair.first) : pair.first != null) return false;
            if (second != null ? !second.equals(pair.second) : pair.second != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = first != null ? first.hashCode() : 0;
            result = 31 * result + (second != null ? second.hashCode() : 0);
            return result;
        }
    }

    private UnoExpFilterRegistry() {
    }
}
